#ifndef GENERIK_LOGGER_H
#define GENERIK_LOGGER_H

#include "../../macros.h"

#define GENERIK_LOGGER \
	generik::logging::logger::instance()

#define GENERIK_LOG_(TYPE, MESSAGE) \
	{ \
		std::stringstream buf; \
		buf<<MESSAGE; \
		GENERIK_LOGGER->TYPE(__PRETTY_FUNCTION__, __FILE__, __LINE__, buf.str()); \
	}

#define GENERIK_DEBUG(MESSAGE) \
	GENERIK_LOG_(debug, MESSAGE)
	
#define GENERIK_LOG(MESSAGE) \
	GENERIK_LOG_(log, MESSAGE)

#define GENERIK_ERROR(MESSAGE) \
	GENERIK_LOG_(error, MESSAGE)

#define GENERIK_WARN(MESSAGE) \
	GENERIK_LOG_(warn, MESSAGE)

#define GENERIK_LOGGER_INIT \
	generik::logging::initialiser _generik_logging_initialiser

/*
call with
GENERIK_LOGGER_INIT; 							: initialises the top-level logger
GENERIK_LOGGER_INIT("streamname"); 				: sets a stream name
GENERIK_LOGGER_INIT((int)5); 					: sets the default loglevel
GENERIK_LOGGER_INIT("streamname", (int)6); 		: sets both
*/

#include <cxxabi.h>
#include <iostream>
#include <sstream>
#include <unistd.h>
#include <pthread.h>

namespace generik
{
	namespace logging
	{
	
		struct logger
		{
			static logger* instance()
			{
				static logger* instance_ = 0;
				if (!instance_)
				{
					instance_ = new logger;
				}
				return instance_;
			}
			
			enum loglevel
			{
				TRACE_DEBUG = 0,
				TRACE_LOG   = 1,
				TRACE_WARN  = 2,
				TRACE_ERROR = 3,
			};

			std::ostream* stream_;
			loglevel loglevel_;

			logger(loglevel defaultlevel = TRACE_DEBUG, std::ostream* defaultstream = &std::cerr)
			: stream_(defaultstream)
			, loglevel_(defaultlevel)
			{}
			
			void set_loglevel(loglevel minimum = TRACE_DEBUG)
			{
				loglevel_ = minimum;
			}
			
			std::ostream* get_stream()
			{
				return stream_;
			}
			
			std::string itoa(int value_in)
			{
				std::stringstream string;
				string<<value_in;
				return string.str();
			}
			std::string at()
			{
				return itoa(clock());
			}
			int pid()
			{
				return getpid();
			}
			int tid()
			{
				pthread_t tid = pthread_self();
				unsigned int pself = *((unsigned int*)&tid);
				return pself;
			}

			void log_(loglevel level, char prefix, const std::string& func, const std::string& file, int line, const std::string& msg)
			{
				if (level >= loglevel_)
				{
					std::ostream* stream = get_stream();
					(*stream)<<prefix<<" "<<at()<<" \""<<func<<"\" ["<<file<<"@"<<line<<"]: "<<msg<<std::endl;
				}
			}
			void log_(loglevel level, char prefix, const std::string& func, const std::string& file, int line)
			{
				if (level >= loglevel_)
				{
					std::ostream* stream = get_stream();
					(*stream)<<prefix<<" "<<at()<<" \""<<func<<"\" ["<<file<<"@"<<line<<"]"<<std::endl;
				}
			}
			void log_(loglevel level, char prefix, const std::string& func)
			{
				if (level >= loglevel_)
				{
					std::ostream* stream = get_stream();
					(*stream)<<prefix<<" "<<at()<<" \""<<func<<"\""<<std::endl;
				}
			}
			
			void debug(const std::string& func, const std::string& file, int line, const std::string& msg)
			{
				// somehow this should have a level associated with it, in this case 0
				// We should only log this if the current log level is less than or equal to 0.
				log_(TRACE_DEBUG, ':', func, file, line, msg);
			}
			void log(const std::string& func, const std::string& file, int line, const std::string& msg)
			{
				// same here, this should be level 1
				log_(TRACE_LOG, '*', func, file, line, msg);
			}
			void warn(const std::string& func, const std::string& file, int line, const std::string& msg)
			{
				// same here, this should be level 2
				log_(TRACE_WARN, '!', func, file, line, msg);
			}
			void error(const std::string& func, const std::string& file, int line, const std::string& msg)
			{
				// same here, this should be level 3
				log_(TRACE_ERROR, 'E', func, file, line, msg);
			}
		};
		
		struct loglevel_handler
		{
			logger::loglevel prev_;
			logger::loglevel new_;
			
			void init(const std::string& level)
			{
				//std::cerr<<"read GENERIK_LOG_LEVEL as ["<<loglevel<<"]"<<std::endl;
				if (level == "TRACE_DEBUG") 	new_ = logger::TRACE_DEBUG; 
				else
				if (level == "TRACE_LOG") 		new_ = logger::TRACE_LOG;
				else
				if (level == "TRACE_WARN") 		new_ = logger::TRACE_WARN;
				else
				if (level == "TRACE_ERROR") 	new_ = logger::TRACE_ERROR;
				else
				{
					//std::cerr<<"not setting new_, unknown loglevel"<<std::endl;
				}
				init();
			}
			
			void init(logger::loglevel level)
			{
				new_ = level;
				init();
			}
			
			void init()
			{
				GENERIK_LOGGER->loglevel_ = new_;
			}
			
			loglevel_handler(logger::loglevel level = logger::TRACE_DEBUG)
			: prev_(level)
			, new_(logger::TRACE_DEBUG)
			{
				init();
			}
			
			~loglevel_handler()
			{
				GENERIK_LOGGER->loglevel_ = prev_;
			}
			
		};
		
		struct stream_handler
		{
			std::ostream* prev_;
			std::ostream* new_;
			bool should_delete_;
			
			void resolve_stream(const std::string& name, std::ios_base::openmode mode = std::ios_base::out)
			{
				std::cerr<<"resolving stream "<<name<<std::endl;
				if (name == "STDERR")
				{
					//std::cerr<<"setting stream_ to std::cerr"<<std::endl;
					new_ = &std::cerr;
					should_delete_ = false;
				}
				else
				if (name == "STDOUT")
				{
					//std::cerr<<"setting stream_ to std::cout"<<std::endl;
					new_ = &std::cout;
					should_delete_ = false;
				}
				else
				{
					//std::cerr<<"setting stream_ to file"<<std::endl;
					std::ofstream* check = new std::ofstream(name.c_str(), mode);
					if (!check->is_open())
					{
						//std::cerr<<"could not open file ["<<name<<"] for writing"<<std::endl;
						delete check;
						new_ = prev_;
						should_delete_ = false;
					}
					else
					{
						//std::cerr<<"succesfully openend ["<<name<<"] for writing"<<std::endl;
						new_ = check;
						should_delete_ = true;
					}
				}
			}
			
			void init(const std::string& name, std::ios_base::openmode mode = std::ios_base::out)
			{
				// find or create the new stream
				resolve_stream(name, mode);
				// set the logger to use the stream
				GENERIK_LOGGER->stream_ = new_;
			}
			
			stream_handler(const std::string& name, std::ios_base::openmode mode = std::ios_base::out)
			: prev_(GENERIK_LOGGER->stream_)
			, new_(0)
			, should_delete_(false)
			{
				init(name, mode);
			}
			
			stream_handler()
			: prev_(GENERIK_LOGGER->stream_)
			, new_(0)
			, should_delete_(false)
			{}
			
			~stream_handler()
			{
				GENERIK_LOGGER->stream_ = prev_;
				if (should_delete_)
				{
					delete new_;
				}
			}
		};
		
		struct initialiser
		{
			std::string streamname_;
			std::string levelname_;
			
			stream_handler stream_handler_;
			loglevel_handler loglevel_handler_;
			
			initialiser(const std::string& name = "STDERR", const std::string& level = "TRACE_DEBUG")
			: streamname_(name)
			, levelname_(level)
			{
				get_logstream();
				get_loglevel();
			}
			
			initialiser(const std::string& name, logger::loglevel level)
			: streamname_(name)
			{
				get_logstream();
				loglevel_handler_.init(level);
			}
			
			initialiser(logger::loglevel level)
			: streamname_("STDERR")
			{
				get_logstream();
				loglevel_handler_.init(level);
			}
			
			void get_logstream(const std::string& envname = "GENERIK_LOG_STREAM")
			{
				char* logstreamchar = getenv(envname.c_str());
				if (logstreamchar)
				{
					streamname_ = logstreamchar;
				}
				stream_handler_.init(streamname_);
			}
			
			void get_loglevel(const std::string& envname = "GENERIK_LOG_LEVEL")
			{
				char* levelchar = getenv(envname.c_str());
				if (levelchar)
				{
					levelname_ = levelchar;
				}
				loglevel_handler_.init(levelname_);
			}
			
		};
	
	}
}

#endif
